"""
delta对冲交易
"""
import dataclasses, time
import logging
from typing import Optional, List, Dict

from common.exchanges import ExchangePriServiceBase
from common.exchanges.base import ExchangeError, ExchangeErrorCode
from common.exchanges.contants import OrderState, OrderType
from common.exchanges.entities import Instrument, Future, IndexPair, Order
from common.exchanges.manager import ExchangeServiceManager
from common.models.account import TradeTask, Account
from common.utils.tools import format_num
from web import session_scope
from web.scheduler import scheduler
from web.service.account import AccountService


@dataclasses.dataclass
class Leg:
    instrument: str = None
    total_amount: float = None
    direction: str = None
    kind: str = None
    order_id: Optional[str] = None
    filled_amount: float = 0
    avg_price: float = 0
    current_step: int = 0
    step_amount: float = None
    error: str = None
    order: Optional[Order] = None
    last_ts: int = 0

    @classmethod
    def deserialize(cls, params):
        return cls(**params)

    def serialize(self):
        return dataclasses.asdict(self)

    def get_step_amount(self, total_step, step):
        if self.current_step >= step or self.current_step >= total_step:
            return 0
        if self.current_step + 1 < total_step:
            if self.step_amount and self.step_amount > 0:
                need_fill_amount = min((self.current_step + 1) * self.step_amount, self.total_amount)
            else:
                need_fill_amount = self.total_amount / total_step * (self.current_step + 1)
        else:
            need_fill_amount = self.total_amount
        return max(need_fill_amount - self.filled_amount, 0)

    def is_step_completed(self, step):
        return self.current_step >= step

    def can_order(self, step):
        return self.current_step < step and not self.order_id

    def update_by_empty(self):
        self.order_id = None
        self.last_ts = int(time.time() * 1000)
        self.current_step += 1

    def on_error(self, error):
        self.error = error


class TradeInfoState:
    Running = 'running'
    Completed = 'completed'
    Canceling = 'canceling'
    Canceled = 'canceled'
    Failing = 'failing'
    Failed = 'failed'

    @classmethod
    def is_end(cls, state):
        return state in [cls.Canceled, cls.Failed, cls.Completed]

    @classmethod
    def is_ending(cls, state):
        return state in [cls.Failing, cls.Canceling,]

    @classmethod
    def to_end(cls, state):
        if not cls.is_ending(state):
            raise ValueError(f'can not change state {state} to end')
        if state == cls.Failing:
            return cls.Failed
        if state == cls.Canceling:
            return cls.Canceled


class TradeMode:
    Normal = 1      # 普通模式
    Middle = 2      # 中速模式
    Fast = 3        # 极速模式


@dataclasses.dataclass
class TradeInfo:

    legs: List[Leg]
    is_balance: bool = True     # 默认平衡下单
    total_step: int = 1
    current_step: int = 1
    update_ts: int = 0
    state: str = TradeInfoState.Running
    trade_mode: int = TradeMode.Normal
    ending_times: int = 0   # 结束次数

    @classmethod
    def deserialize(cls, params):
        order_datas = params.pop('legs', [])
        orders = [Leg.deserialize(order_data) for order_data in order_datas]
        return cls(**params, legs=orders)

    def serialize(self):
        data = dataclasses.asdict(self)
        data['legs'] = [i.serialize() for i in self.legs]
        return data

    def refresh_running_state(self):
        if self.state != TradeInfoState.Running:
            return False
        has_error = any([leg.error for leg in self.legs])
        if has_error:
            self.state = TradeInfoState.Failing
            return True
        if all([order_data.current_step >= self.current_step for order_data in self.legs]):
            if self.current_step >= self.total_step:
                self.state = TradeInfoState.Completed
            self.current_step += 1
            return True

    def get_error(self):
        errors = filter(lambda i: i.error, self.legs)
        return list(errors)

    def set_update_timestamp(self):
        order_update_ts = max([order_data.last_ts for order_data in self.legs])
        if self.update_ts < order_update_ts:
            self.update_ts = order_update_ts
            return True
        return False


class MultiLegTradeExecutor:

    def __init__(self, task: TradeTask, account=None, logger=None):
        self._task = task
        self._job = None

        self._account = account
        self._account_service: Optional[ExchangePriServiceBase] = None
        self._inst_info_map: Dict[str, Instrument] = {}
        self._trade_info: Optional[TradeInfo] = None
        self._logger = logger or logging.getLogger('executor')
        self._is_installed = False

    @classmethod
    def check_params(cls, user, account: Account, params):
        pass

    def install(self):
        """ 将常用的信息加载入内存，并启动相关任务 """
        if not self._account:
            self._account = Account.query.filter_by(id=self._task.account_id).first()

        if not self._account:
            end_kwargs = {
                'status': TradeTask.TaskStatus.Failed,
                'error_msg': 'trade account does not exist.'
            }
            with session_scope() as session:
                session.query(TradeTask).filter_by(id=self._task.id).update(end_kwargs)
            return False

        self._account_service = ExchangeServiceManager.pri_factory(
            self._account.exchange, AccountService.account2auth_info(self._account),
        )
        self._account_service.ensure_catcher_run()
        self._trade_info = TradeInfo.deserialize(self._task.data)

        pub_service = ExchangeServiceManager.pub_factory(exchange=self._task.exchange)
        for leg in self._trade_info.legs:
            self._inst_info_map[leg.instrument] = pub_service.instrument_info(leg.instrument, kind=leg.kind)

        if scheduler.scheduler.get_job(self._task.id):
            self._job = scheduler.scheduler.modify_job(self._task.id, self.run, trigger='interval', seconds=3,)
        else:
            self._job = scheduler.add_job(self._task.id, self.run, trigger='interval', seconds=3, )
        self._logger.info(f'Installed task {self._task.id} with info {self._trade_info}')
        self._is_installed = True
        return True

    def uninstall(self):
        scheduler.remove_job(self._task.id)

    def is_trade_ended(self):
        return TradeInfoState.is_end(self._trade_info.state)

    def _handel_ending_event(self):
        # 检查是否有订单需要做撤单处理
        is_completed = True
        self._trade_info.ending_times += 1
        for leg in self._trade_info.legs:
            if not leg.order_id:
                continue
            order = None
            try:
                order = self._account_service.order_state(leg.order_id, instrument=leg.instrument, kind=leg.kind)
            except Exception as e:
                self._logger.exception(f'can not get order info for {leg.order_id} {e}')
            if order.state == OrderState.Open:
                try:
                    order = self._account_service.cancel_order(instrument=leg.instrument, order_id=leg.order_id, account_type=order.account_type)
                    cancel_order_success = order.state != OrderState.Open
                except Exception as e:
                    self._logger.warning(f'cancel order except: {e}')
                    cancel_order_success = False
                if not cancel_order_success:
                    is_completed = False

            if order:
                self._update_leg_state(leg, order, None)
        if not is_completed and self._trade_info.ending_times >= 3:
            is_completed = True
        return is_completed

    def _persist_task(self, ):
        if not self._is_installed:
            raise ValueError('trader has not persisted.')
        with session_scope() as session:
            try:
                self._task.data = self._trade_info.serialize()
                kwargs = {'data': self._trade_info.serialize()}
                if self._trade_info.state in [TradeInfoState.Canceled, TradeInfoState.Failed]:
                    kwargs['status'] = TradeTask.TaskStatus.Failed
                elif self._trade_info.state == TradeInfoState.Completed:
                    kwargs['status'] = TradeTask.TaskStatus.Completed
                if self._trade_info.state == TradeInfoState.Failed:
                    error_msgs = [leg.error for leg in self._trade_info.legs if leg.error]
                    kwargs['error_msg'] = error_msgs[0]

                session.query(TradeTask).filter_by(id=self._task.id).update(kwargs)
                session.commit()
                # self._logger.info(f'persist trade info {self._task.data} to task {self._task.id}')
            except Exception as e:
                self._logger.exception(f'persist task for trade info {self._trade_info} failed: {e}')
                session.rollback()
            finally:
                session.close()

    def stop(self):
        if self._trade_info.state == TradeInfoState.Running:
            self._trade_info.state = TradeInfoState.Canceled
        return True

    def run(self):
        if not self._is_installed:
            self.install()

        need_persist = False

        if TradeInfoState.is_ending(self._trade_info.state):
            is_completed = self._handel_ending_event()
            if is_completed:
                self._trade_info.state = TradeInfoState.to_end(self._trade_info.state)

        if TradeInfoState.is_end(self._trade_info.state):
            self.uninstall()
            need_persist = True

        if self._trade_info.state == TradeInfoState.Running:
            trade_mode = self.step_trade_mode()
            for order_data in self._trade_info.legs:
                # 对每条腿执行交易
                self._trade_leg(order_data, self._trade_info.current_step, self._trade_info.total_step, trade_mode)

            # 更新总的交易信息
            self._trade_info.refresh_running_state()
            if self._trade_info.set_update_timestamp():
                need_persist = True

        if need_persist:
            self._persist_task()
        self._logger.info(f'complete trade {self._trade_info} interval execute...')

    def step_trade_mode(self,):
        """交易所模式"""
        default_mode = self._trade_info.trade_mode
        current_step = self._trade_info.current_step
        step_completed_legs = sorted([
            leg for leg in self._trade_info.legs if leg.current_step >= current_step], key=lambda i: i.last_ts)
        first_completed_ts = None
        if step_completed_legs:
            first_completed_ts = step_completed_legs[0].last_ts
        now_ts = int(time.time() * 1000)
        if first_completed_ts and first_completed_ts < now_ts - 30000:
            return max(default_mode, TradeMode.Middle)
        if first_completed_ts and first_completed_ts < now_ts - 60000:
            # 暂不支持极速模式
            return max(TradeMode.Middle, default_mode)
        return default_mode

    def _calc_price(self, instrument, kind, direction, tick_step, trade_mode):
        pub_service = ExchangeServiceManager.pub_factory(exchange=self._task.exchange)
        ticker = pub_service.ticker(instrument, kind=kind)

        spread = -0.0001 if direction == 'buy' else 0.0001
        if trade_mode == TradeMode.Middle:
            spread = 0

        return format_num(ticker.mark_price * (1 + spread), tick_step)

    def _update_leg_state(self, leg, order, error):
        inst_info = self._inst_info_map[leg.instrument]
        if order:
            leg.order_id = order.order_id
            leg.last_ts = order.update_timestamp

            if order.state in [OrderState.Filled, OrderState.Cancelled, OrderState.Closed]:
                filled_amount = order.filled_amount * inst_info.contract_multi
                if order.filled_amount > 0:
                    if isinstance(inst_info, Future) and inst_info.reversed:
                        filled_amount = filled_amount / order.avg_price if order.avg_price else 0

                    leg.avg_price = \
                        (leg.filled_amount * leg.avg_price + filled_amount * order.avg_price) / \
                        (leg.filled_amount + filled_amount)
                leg.filled_amount += filled_amount
                leg.order_id = None
        if error:
            leg.error = error

        if order and order.state == OrderState.Filled:
            leg.current_step += 1

    def _trade_leg(self, leg: Leg, step, total_step, trade_mode):
        inst_info = self._inst_info_map[leg.instrument]

        index_price = ExchangeServiceManager.pub().index_price(
            IndexPair(base=inst_info.base_currency, quote=inst_info.quote_currency)
        ).price

        order, error = None, None

        if leg.order_id:
            # 更新订单信息
            try:
                order = self._account_service.order_state(
                    leg.order_id, instrument=leg.instrument, kind=leg.kind)
            except Exception as e:
                error = f'can not get order for {leg.instrument}, history order id is {leg.order_id}'
                self._logger.exception(f'{error}, error msg is {e}')

        if order:
            # 更新订单信息
            if order and order.state == OrderState.Open:
                new_price = self._calc_price(
                    order.instrument, leg.kind, leg.direction, inst_info.tick_size_step, trade_mode
                )
                spread = (new_price - order.price if leg.direction == 'buy' else order.price - new_price) / index_price
                # 更新价格
                if spread > 0.0001:
                    try:
                        order = self._account_service.reset_order(
                            order.order_id, size=order.amount, price=new_price, kind=leg.kind,
                            instrument=leg.instrument,
                        )
                    except Exception as e:
                        error = f'reset order {order} to price {new_price} failed.'
                        self._logger.exception(f'{error}, error msg is {e}')
            elif order and order.state == OrderState.Cancelled:
                error = f'{leg.instrument} order canceled unexpected'
            self._update_leg_state(leg, order, error)
            # leg.update_leg_state(order, inst_info, error)

        if leg.can_order(step):
            amount = leg.get_step_amount(total_step, step) * (1 if leg.direction == 'buy' else -1) / inst_info.contract_multi
            price = self._calc_price(
                leg.instrument, leg.kind, leg.direction, inst_info.tick_size_step, trade_mode)
            if isinstance(inst_info, Future) and inst_info.reversed:
                amount = amount * price
            amount = format_num(amount, inst_info.trade_size_step)
            if abs(amount) < inst_info.min_trade_size:
                self._logger.warning(f'{self._task.id} leg {leg.instrument} amount {amount} is invalid')
                leg.update_by_empty()
            else:
                try:
                    self._logger.info(f'begin create order for {leg.instrument}, price is {price}, amount is {amount}')
                    order = self._account_service.create_order(
                        leg.instrument, amount, price, order_type=OrderType.Limit, kind=inst_info.kind, post_only=True,
                    )
                    self._logger.info(f'create order for {inst_info.instrument_name}, detail is {order}')
                except ExchangeError as e:
                    self._logger.error(f'create order for {leg.instrument} throw exchange error: {e}')
                    # 特别处理因post only 导致下单失败的问题
                    if e.code == ExchangeErrorCode.PostOnlyOrderRejected.code:
                        self._logger.warning(f'create post only order failed: {e.msg}')
                    else:
                        error = f'create order for {leg.instrument} failed: {e.msg}'
                except Exception as e:
                    error = f'create order for {leg.instrument} failed'
                    self._logger.exception(f'{error}, error msg is {e}')
            self._update_leg_state(leg, order, error)




